원본 항목: Part 1-2. 외부제어·우회 계열 — 8. 보호 메커니즘을 우회할 수 있는 입력값 변조CWE-807repo 폴더: sd08_cookietamper/ (빌드·실행은 §3-1)
목표 시간: 1회차 15분 / 2회차 9분 / 3회차 6분mock_session 은 서버가 로그인 시점에 세션ID→권한을 저장해 두는 저장소를 흉내낸다.
클라이언트(쿠키)는 이 저장소를 조회하는 “열쇠”만 들고 있을 뿐, 열쇠에 적힌 글씨를 아무리 바꿔도 저장소 내용은 바뀌지 않는다.
이 드릴의 문서 구성 (SD01~SD58 공통)
① 요구사항서 — 어떤 코드가 뚫려 있고, 무엇을 만드나.
② pseudo code 템플릿 — Bad 와 Good 의 뼈대이고, 회차마다 가린다.
③ 테스트 코드 — 공격은 막히고 정상은 통과해야 끝이다.
④ 자주 하는 실수
⑤ 모범답안 — 맨 뒤에 접어 둔다.
변형 A/B/C 는 같은 치환 규칙을 다른 소재로 다시 치는 것이다.
0. 이 드릴로 체화할 것
- return (cookie_role != NULL && strcmp(cookie_role, "admin") == 0); /* 클라이언트 값으로 직접 판단 */+ return mock_session_lookup_role(cookie_session_id) == ROLE_ADMIN; /* 서버 저장소 조회 결과로 판단 */
이 계열(SD08~SD10)은 삽입 계열과 달리 “값을 검증”하는 게 아니라 “판단 근거를 클라이언트가 아니라 서버에 두는가” 가 치환의 핵심이다.
회차별 진행
회차
변형
보는 것
순서
1회차
A 관리자 기능 접근 판정
① 요구사항서 ② pseudo code 전체
1. Bad 를 그대로 쳐서 취약점을 관찰한다. 2. Good pseudo 를 한 줄씩 코드로 바꾼다. 3. ③ 테스트를 실행한다.
2회차
B 결제 승인 권한 판정
① 요구사항서 ② pseudo code 의 번호와 제목만 (Level 2)
1. Bad 를 치고 취약점을 확인한다. 2. 번호만 보고 Good 을 쓴다. 3. ③ 테스트를 실행한다.
3회차
C 세션 만료 추가
① 요구사항서만
1. ③ 테스트를 먼저 읽는다. 2. 백지에서 Bad 부터 Good 까지 재현한다.
1회차에 막히면 같은 변형을 한 번 더 친다.
3회차를 통과하면 A 를 백지로 다시 친다.
1. 요구사항서
1-0. 환경 코드 — mock_session (미리 제공, 타이핑하지 않음)
sd08_cookietamper/src/mock_session.h / mock_session.c — repo에 이미 있다
/* mock_session.c */#include <string.h>#include "mock_session.h"typedef struct { const char *sid; role_t role; } session_entry_t;static session_entry_t g_sessions[8];static int g_session_count = 0;void mock_session_register(const char *session_id, role_t role){ if (g_session_count < 8) { g_sessions[g_session_count].sid = session_id; g_sessions[g_session_count].role = role; g_session_count++; }}role_t mock_session_lookup_role(const char *session_id){ int i; if (session_id == NULL) return ROLE_GUEST; for (i = 0; i < g_session_count; i++) if (strcmp(g_sessions[i].sid, session_id) == 0) return g_sessions[i].role; return ROLE_GUEST;}
1-A. 변형 A — 관리자 기능 접근 판정
요청에 실려온 쿠키 값으로 “이 요청이 관리자 권한인지”를 판정한다.
실제 서비스라면 로그인 시점에 세션ID가 발급되고, 그 세션ID에 역할(role)이 매핑되어 서버에 저장된다.
클라이언트는 세션ID만 쿠키로 들고 다닌다.
함수
int is_admin_bad(const char *cookie_role);int is_admin_good(const char *cookie_session_id);
신뢰 경계
값
출처
검증 없이 흘러가는 곳
쿠키 값(role 또는 session_id)
외부(클라이언트가 보낸 요청)
관리자 권한 판정
공격 입력표
상황
Bad 결과
이유
쿠키 role=admin (조작)
관리자로 판정됨
쿠키 값은 프록시 도구 하나로 누구나 바꿀 수 있다
등록되지 않은 세션ID를 "admin" 으로 지정 (Good 대상 공격)
거부됨
서버 저장소에 없는 값이므로 ROLE_GUEST 로 취급
1-B. 변형 B — 결제 승인 권한 판정
ROLE_ADMIN 대신 별도 can_approve_payment 불리언 플래그를 세션에 저장하는 구조로 변경 — enum 하나로 부족할 때 세션 구조체를 어떻게 넓힐지 연습
1-C. 변형 C — 세션 만료 추가
mock_session_register 에 만료 시각을 추가하고, mock_session_lookup_role 이 만료된 세션은 ROLE_GUEST 로 취급하도록 mock 자체를 확장(이번엔 mock까지 직접 고쳐본다)
2. pseudo code 템플릿 — 보고 쳐서 익힌다
사용법
변형별로 아래 pseudo code 를 소스 파일에 주석만 복사한다.
코드는 주석을 보고 내가 쓴다.
다 쓰면 [ ] 를 x 로 바꾼다.
막히면 §5 모범답안(A)의 해당 단계만 본다.
2-A. is_admin_good — Level 1 (1회차, 전부 본다)
먼저 Bad 를 그대로 쳐서 §3 의 test_bad_is_vulnerable 이 통과하는지(= 실제로 뚫리는지) 확인한다.
그다음 Good 을 쓴다.
int is_admin_bad(const char *cookie_role){ /* 1. return cookie_role != NULL && strcmp(cookie_role, "admin") == 0 * ← 클라이언트가 보낸 값으로 직접 판단 */}int is_admin_good(const char *cookie_session_id){ /* [ ] 1. 매개변수 의미를 바꾼다 * role 이 아니라 session id 만 받는다 */ /* [ ] 2. 서버 저장소에서 역할을 조회한다 * mock_session_lookup_role(cookie_session_id) */ /* [ ] 3. return (조회된 역할 == ROLE_ADMIN) * 없는 세션 / NULL 은 ROLE_ADMIN 이 아니다 */}
2-B. is_admin_good — Level 2 (2회차, 번호와 제목만 본다)
int is_admin_good(const char *cookie_session_id){ /* [ ] 1. 매개변수 의미를 바꾼다 */ /* [ ] 2. 서버 저장소에서 역할을 조회한다 */ /* [ ] 3. return (조회된 역할 == ROLE_ADMIN) */}
2-C. is_admin_good — Level 3 (3회차, 백지)
int is_admin_good(const char *cookie_session_id){ /* 아무것도 안 본다. * §1 요구사항서와 §0 의 치환 규칙만 보고 친다. * * 막히면 아래만 찾아본다. * - `ROLE_ADMIN` 이 정의된 `mock_session.h` * - 없는 세션 id 의 반환값 */}
막혔을 때 찾는 순서
컴파일 에러가 나면 에러 문구를 본다.
unknown type name / implicit declaration 이면 include 누락이다.
man 3 <함수> 를 연다.
SYNOPSIS 맨 위의 #include 를 확인한다.
로직이 막히면 §3 테스트의 기대값을 먼저 읽는다.
공격 입력표(§1)와 테스트가 답이다.
눈여겨볼 점: is_admin_good 의 매개변수 이름이 cookie_role 이 아니라 cookie_session_id 로 바뀌었다 — 함수가 받는 데이터의 의미 자체가 달라졌다.
“권한을 나타내는 값”을 클라이언트에서 받지 않고, “누구인지를 가리키는 표”만 받는다는 원칙이 시그니처에 그대로 드러난다.